﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Medianamik.Core.Configuration;
using System.Configuration;
using System.Threading;
using Medianamik.Core.Reports;
using Aron.Weiler;

//Voir http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx

namespace Medianamik.Core.Urls
{
    public class UrlDictionary : IUrlDictionary
    {
        private IPathSegmentFormater _segmentFormater;
        private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
        private IReportManager<UrlDictionaryReport> _reportManagerManager;
        private MedianamikConfigSection _config;

        //private readonly IDictionary<String, IUrlDictionaryEntry> _urls =
        //    new Dictionary<String, IUrlDictionaryEntry>(StringComparer.OrdinalIgnoreCase);

        private readonly MultiKeyDictionary _urls = new MultiKeyDictionary(StringComparer.OrdinalIgnoreCase);

        //private IDictionary<String, IUrlDictionaryEntry> Urls
        //{
        //    get
        //    {
        //        return _urls;
        //    }
        //}

        private MultiKeyDictionary Urls
        {
            get
            {
                return _urls;
            }
        }

        private IReportManager<UrlDictionaryReport> ReportManagerManager
        {
            get { return _reportManagerManager; }
        }

        private void Add(IEnumerable<IUrlDictionaryEntry> entries)
        {
            if (UrlDictReport != null)
                UrlDictReport.NumberTotalOfEntries += entries.Count();

            entries.ForEach(AddUniqueEntry);
        }

        public void Initialize(MedianamikConfigSection config, IPathSegmentFormater segmentFormater, IReportManager<UrlDictionaryReport> reportManager)
        {
            _segmentFormater = segmentFormater;
            _config = config ?? (MedianamikConfigSection)ConfigurationManager.GetSection(ConfigurationSectionName.MEDIANAMIK);
            _reportManagerManager = reportManager ?? new UrlDictionayEmailReportManager(_config.SendUrlDictionaryReportTo,
                "UrlDictionary Report from: " + (ConfigurationManager.AppSettings["ProbeWebSiteBaseURL"] ?? ""));

            LoadEntries();

            UrlDictReport.ReportedReportAction = ReportAction.Initialized;
            SendReport();

            Initialized = true;
        }

        public void Initialize(MedianamikConfigSection config)
        {
            Initialize(config, new PathSegmentFormater(), null);
        }

        private void LoadEntries()
        {
            WriteLock(() =>
            {
                Urls.Clear();
                UrlDictReport = new UrlDictionaryReport();

                Add(GetUrlDictionaryEntries());
                Add(GetDefaultDocumentEntries());

                UrlDictReport.NumberOfDistinctEntries = Urls.Count;
            });
        }

        protected virtual IEnumerable<IUrlDictionaryEntry> GetDefaultDocumentEntries()
        {
            var webSites = ConfigManager.GetAll<WebSite>();

            var websitesInstances = (from website in webSites
                                     from instance in website.Instances
                                     select new
                                                {
                                                    DefaultDocument = instance["DefaultDocument"].GetValue<Guid?>(),
                                                    Prefix = instance["Prefix"].GetValue<string>(),
                                                    NodeId = instance.Node.ID,
                                                    CultureName = instance.Culture.Name,
                                                }).ToList();

            var langs = ConfigManager.GetEditionCultures().ToList();

            //Ajout des documents par defaut
            var defaultDocs = (from entry in Urls
                               join wi in websitesInstances on entry.Value.NodeId equals wi.DefaultDocument
                               where string.Equals(entry.Value.Culture.Name, wi.CultureName, StringComparison.InvariantCultureIgnoreCase)
                               where Equals(wi.NodeId, entry.Value.WebsiteId)
                               select new UrlDictionaryEntry(wi.DefaultDocument.Value,
                                 webSites.First(w => w.ID.Equals(wi.NodeId)), entry.Value.Culture, entry.Value.Revision,
                                 entry.Value.Generation, entry.Value.Scheme, entry.Value.Domain,
                                 entry.Value.DomainGroup, entry.Value.DomainCulture, "/", entry.Value.RewritedPath,
                                 entry.Value.ExcludedFromIndexation));

            //Ok on a la liste des documents par défaut mais attention
            //il peut exister deux fois la meme URL par défaut pour deux langues différentes
            //quand le préfixe du website est identique dans les deux langues
            //Il faut donc ordonner la liste en fonction de la priorité des langues dans Medianamik
            var defaultDocumentEntries = defaultDocs
                .Where(d => langs.Any(l => l.CultureInfo == d.Culture))
                .Select(d => new
                     {
                         Doc = d,
                         langs.First(l => l.CultureInfo == d.Culture).Position
                     })
                .OrderBy(d => d.Position)
                .Select(d => d.Doc);

            return defaultDocumentEntries.Cast<IUrlDictionaryEntry>().ToList();
        }

        protected virtual IEnumerable<IUrlDictionaryEntry> GetUrlDictionaryEntries()
        {
            return ConfigManager.GetUrlDictionaryEntries();
        }

        private void SendReport()
        {
            if (_config.SendUrlDictionaryReport)
                ReportManagerManager.Send(UrlDictReport);
        }

        private void AddUniqueEntry(IUrlDictionaryEntry entry)
        {
            if (Urls.ContainsKey(entry.LiteralRequestedUrl))
                UrlDictReport.DuplicatedEntries.Add(entry);
            else
                Urls.Add(entry.LiteralRequestedUrl, entry.NodeIdCultureKey , entry);
        }

        public static UrlDictionary Instance
        {
            get
            {
                return Singleton<UrlDictionary>.Instance;
            }
        }

        protected internal UrlDictionaryReport UrlDictReport
        {
            get;
            set;
        }

        public void Reload()
        {
            if (!Initialized)
                throw new Exception("Reload is called but never been initilized");

            LoadEntries();

            UrlDictReport.ReportedReportAction = ReportAction.Reload;
            SendReport();
        }

        private T ReadLock<T>(Func<T> func)
        {
            _lock.EnterReadLock();
            try { return func(); }
            finally { _lock.ExitReadLock(); }
        }

        private void UpgradeableReadLock(System.Action action)
        {
            _lock.EnterUpgradeableReadLock();
            try { action(); }
            finally { _lock.ExitUpgradeableReadLock(); }
        }

        private void WriteLock(System.Action action)
        {
            _lock.EnterWriteLock();
            try { action(); }
            finally { _lock.ExitWriteLock(); }
        }

        public IUrlDictionaryEntry GetEntry(string path)
        {
            return ReadLock(() =>
                {
                    IUrlDictionaryEntry result;
                    return Urls.TryGetValue(path, out result) ? result : null;
                });
        }

        public IList<IUrlDictionaryEntry> GetEntries(Guid nodeId, CultureInfo culture)
        {
            return ReadLock(() => 
                {
                    if (culture == null)
                    {
                        //return Urls.;
                        return (from url in Urls where url.Value.NodeId.Equals(nodeId) select url.Value).ToList();
                    }

                    return (from url in Urls where url.Value.NodeIdCultureKey.Equals(new MultiKeyDictionaryKey(nodeId, culture))
                            orderby url.Value.DomainCulture.EqualsIgnoreCase(culture.Name)
                            select url.Value).ToList();
                    //return new List<IUrlDictionaryEntry>();
                });
        }

        public bool ContainsUrl(string path)
        {
            return ReadLock(() => Urls.ContainsKey(path));
        }

        public void Remove(Guid nodeId)
        {
            Remove(nodeId, false);
        }

        public void Remove(Guid nodeId, bool doNotRemoveRoot)
        {
            UpgradeableReadLock(() => Remove(doNotRemoveRoot ? 
                 (from url in Urls 
                    where url.Value.NodeId.Equals(nodeId) && url.Value.RequestedUrl.AbsolutePath != "/" 
                    select url.Value).ToList() : 
                 (from url in Urls 
                    where url.Value.NodeId.Equals(nodeId) 
                    select url.Value).ToList()));
        }

        public void Remove(Guid nodeId, CultureInfo culture)
        {
            UpgradeableReadLock(() => Remove((from url in Urls 
                                              where url.Value.NodeId.Equals(nodeId)  && url.Value.Culture.Equals(culture)
                                              select url.Value).ToList()));
        }

        private void Remove(IEnumerable<IUrlDictionaryEntry> entries)
        {
            if (entries.Count() > 0)
            {
                WriteLock(() =>
                {
                    if (UrlDictReport != null)
                    {
                        UrlDictReport.NumberOfRemovedEntries = entries.Count();
                        UrlDictReport.RemovedEntries = entries.ToList();
                    }
                    entries.ForEach(e => Urls.Remove(e.LiteralRequestedUrl));
                });
            }
        }

        public void UpdateUrlsFor(Guid nodeId)
        {
            UrlDictReport = new UrlDictionaryReport();
            UrlDictReport.ReportedReportAction = ReportAction.UpdateNodeEntries;
            UrlDictReport.UpdatedNodeId = nodeId;
            UrlDictReport.NumberTotalOfEntriesBeforeRemove = Urls.Count;

            Remove(nodeId, true);

            var entries = GetUrlDictionayEntries(nodeId);
            var numberOfEntries = entries.Count();

            if (numberOfEntries > 0)
            {
                WriteLock(() =>
                {
                    UrlDictReport.NumberOfAddedEntries = numberOfEntries;
                    UrlDictReport.AddedEntries = entries.ToList();
                    entries.ForEach(AddUniqueEntry);
                    UrlDictReport.NumberTotalOfEntries = Urls.Count;
                });
            }

            SendReport();
        }

        protected virtual IEnumerable<IUrlDictionaryEntry> GetUrlDictionayEntries(Guid nodeId)
        {
            return ConfigManager.GetUrlDictionaryEntries(nodeId);
        }

        public IList<IUrlDictionaryEntry> GetEntries(CultureInfo culture, string domain, string urlFilter)
        {
            return ReadLock(() =>
                            from url in Urls
                            where
                                url.Value.Culture.Equals(culture)
                                && url.Value.Domain == domain
                                && url.Value.Path.Contains(urlFilter.ToLower())
                            select url.Value).ToList();
        }

        public IList<string> GetDomains()
        {
            return ReadLock(() => Urls.Select(e => e.Value.Domain).Distinct(StringComparer.OrdinalIgnoreCase).ToList());
        }

        public int Count
        {
            get { return ReadLock(() => Urls.Count); }
        }

        public IList<IUrlDictionaryEntry> GetAllEntries()
        {
            return GetAllEntries(IndexationModes.All);
        }

        public IList<IUrlDictionaryEntry> GetAllEntries(IndexationModes indexation)
        {
            switch (indexation)
            {
                case IndexationModes.Excluded:
                    return ReadLock(() => Urls.Where(u => u.Value.ExcludedFromIndexation).Select(x => x.Value).ToList());
                case IndexationModes.Indexed:
                    return ReadLock(() => Urls.Where(u => !u.Value.ExcludedFromIndexation).Select(x => x.Value).ToList());
                default:
                    return ReadLock(() => Urls.Select(x => x.Value).ToList());
            }
        }

        public bool Contains(IUrlDictionaryEntry entry)
        {
            return ReadLock(() => Urls.ContainsKey(entry.LiteralRequestedUrl));
        }

        public string BuildUniqueRelativeUrlWithoutPrefixAndExtension(PathSegmentCollection segments, CultureInfo culture, WebSite website)
        {
            website.DefaultCulture = culture;

            var path = UrlHelper.GetPathFromSegments(_segmentFormater, segments);

            var url = new MedianamikUrl(website, culture, path, false);

            if (!UrlIsUniqueForWebSite(url))
            {
                if (!UrlExistsForCulture(url))
                {
                    //Les autre pages existent dans d'autres cultures
                    //On regarde si on peut ajouter le CultureName au nom de la page
                    var modifiedUrl = new MedianamikUrl(website, culture, 
                        url.OriginalPath + "-" + culture.Name.ToLowerInvariant(), false);

                    if (UrlIsUniqueForWebSite(modifiedUrl))
                        return modifiedUrl.OriginalPath;
                }

                while (true)
                {
                    var modifiedUrl = new MedianamikUrl(website, culture, 
                        url.OriginalPath  + "-" + Guid.NewGuid().ToString("N").Substring(0, 6), false);
                    
                    if (UrlIsUniqueForWebSite(modifiedUrl))
                        return modifiedUrl.OriginalPath;
                }
            }

            return url.OriginalPath;
        }
 
        public bool UrlIsUniqueForWebSite(IMedianamikUrl url)
        {
            return ReadLock(() => !Urls.Any(entry => entry.Value.Path.Equals(url.Path, StringComparison.OrdinalIgnoreCase) &&
                                                     entry.Value.WebsiteId.Equals(url.WebsiteId)));
        }

        public bool UrlIsUniqueForNode(IMedianamikUrl url, Guid nodeId)
        {
            return ReadLock(() => !Urls.Any(entry => entry.Value.Path.Equals(url.Path, StringComparison.OrdinalIgnoreCase) &&
                entry.Value.WebsiteId.Equals(url.WebsiteId) && !entry.Value.NodeId.Equals(nodeId)));
        }

        public bool Initialized { get; private set; }

        private bool UrlExistsForCulture(IMedianamikUrl url)
        {
            return GetCulturesForPath(url.Path).Any(c => c.Equals(url.Culture));
        }

        private CultureInfo[] GetCulturesForPath(string path)
        {
            return GetEntriesForRequestedPath(path).Select(entry => entry.Culture).ToArray();
        }

        private IEnumerable<IUrlDictionaryEntry> GetEntriesForRequestedPath(string path)
        {
            return ReadLock(() => Urls.Where(entry => 
                entry.Value.Path.Equals(path, StringComparison.OrdinalIgnoreCase)).Select(d =>d.Value).ToList());
        }

        public bool UrlIsValid(string realPath)
        {
            return Regex.IsMatch(realPath, "^(/[0-9a-zA-Z_]+)+((/[0-9a-zA-Z_]+)*|(-[0-9a-zA-Z_]+)*)*(\\.[a-zA-Z]+)?$");
            //Doit commencer par un /
            //Ne doit pas contenir de répétitions de -
            //Peut avoir une extension .html, .apsx, etc.
            //Voir Maxime si modification
        }
    }
}
